import { Asset, AssetManager, ImageAsset, JsonAsset, Node, Prefab, Size, Sprite, SpriteFrame, Texture2D, Widget, __private, assetManager, director, error, find, instantiate, isValid, sp } from "cc";
import { App } from "../App";
import BaseScene from "./BaseScene";
import { WanderCfg, WanderCommon } from "./Defines";
import Log from "./LogMgr";
import Pool from "./PoolMgr";
import Singleton from "./Singleton";

interface IBundleInfo {
    bundles: string[];
    prefabs: { [bdlname: string]: { [pfbname: string]: string } };
}

class ResMgr extends Singleton {

    /**
     * 进入场景
     * @param name 场景名称
     * @param onProgress 进度回调。传值：先进行预加载在进场景，不传值：直接进场景
     */
    public runScene(name: string, onProgress?: (progress: number) => void) {
        this._sceneReady = false;
        if (onProgress) {
            let progress = 0;
            director.preloadScene(name, (c, t) => {
                let p = c / t;
                if (p > progress) {
                    progress = p;
                    onProgress(progress);
                }
            }, err => {
                if (err) {
                    Log.e(err);
                    // 恢复
                    this._sceneReady = true;
                    return;
                }
                director.loadScene(name);
            });
        } else director.loadScene(name);
    }

    /**
     * 当 BaseScene start(onStart) 初始化
     * @param scene
     */
    public initScene(scene: BaseScene) {
        this._currPreloadedScene = scene;
        this._sceneReady = true;
    }

    public getScene<T>(): T {
        return this._currPreloadedScene as T;
    }

    public isSceneReady(): boolean {
        return this._sceneReady;
    }

    /**
     * 手动加载预制体
     * @param bdlname 包名
     * @param pfbname 预制体名称
     * @param autorelease 在场景切换的时候是否自动释放
     * @returns
     */
    public manualLoadPrefab(bdlname: string, pfbname: string, autorelease = true): Promise<Prefab> {
        return new Promise((res, rej) => {
            const path = (this.app.bundleInfo.json as IBundleInfo).prefabs[bdlname][pfbname];
            if (!path) {
                rej(new Error(`${pfbname} cannot find any bundle`));
                return;
            }

            this.manualLoadAny(bdlname, path, Prefab, autorelease, (err, asset) => {
                if (err) return rej(err);
                res(asset!);
            });
        });
    }

    public loadRemoteSpine(spine: sp.Skeleton, path: string, onComplete: () => void, autorelease = true) {
        const cfg = this.app.gameCfg.json as WanderCfg.IGame;
        let url = `${cfg.cdn}/${path}`;

        const arr = url.split("/");
        let filename: string = arr[arr.length - 1];
        let imageUrl: string = url + "/" + filename + ".png";
        let skeUrl: string = url + "/" + filename + ".json";
        let atlasUrl: string = url + "/" + filename + ".atlas";

        assetManager.loadAny([{ url: atlasUrl, ext: '.atlas' }, { url: skeUrl, ext: '.json' }], (err, assets) => {
            if (err) return error(err);

            let assetData = this._remoteSpineAssets.get(url);
            if (!assetData) {
                (assets[0] as Asset).addRef();
                (assets[1] as Asset).addRef();
            }

            this._remoteSpineAssets.set(url, {
                atlas: assets[0],
                json: assets[1],
                autorelease
            });

            assetManager.loadRemote<ImageAsset>(imageUrl, (err, asset) => {
                if (err) return error(err);

                let assetData = this._remoteSpineAssets.get(url);
                if (assetData && !assetData.png) {
                    assetData.png = asset;
                    asset.addRef();
                }

                let skeletonData = new sp.SkeletonData();
                skeletonData.atlasText = assets[0];
                skeletonData.skeletonJson = assets[1];
                let texture = new Texture2D();
                texture.image = asset;
                skeletonData.textures = [texture];
                skeletonData.textureNames = [filename + '.png'];

                if (isValid(spine) && isValid(spine.node.parent)) {
                    spine.skeletonData = skeletonData;
                    spine.premultipliedAlpha = false;
                    onComplete();
                }
            })
        });
    }

    // assetManager 会缓存加载的资源的
    private _remoteSpineAssets = new Map<string, { json: Asset, atlas: Asset, png?: Asset, autorelease: boolean }>();

    public setRemoteSpriteFrame(spr: Sprite, path: string, autorelease = true) {
        const cfg = this.app.gameCfg.json as WanderCfg.IGame;
        let url = `${cfg.cdn}/${path}`;
        let assetData = this._remoteImageAssets.get(url);
        if (assetData) {
            if (isValid(spr)) {
                let frame = new SpriteFrame();
                let texture = new Texture2D();
                texture.image = assetData.imageAsset;
                frame.texture = texture;
                spr.spriteFrame = frame;
            }
            return;
        }
        assetManager.loadRemote<ImageAsset>(url, (err, asset) => {
            if (err) return error(err);
            asset.addRef();
            this._remoteImageAssets.set(url, { imageAsset: asset, autorelease });
            if (isValid(spr)) {
                let frame = new SpriteFrame();
                let texture = new Texture2D();
                texture.image = asset;
                frame.texture = texture;
                spr.spriteFrame = frame;
            }
        });
    }

    private _remoteImageAssets = new Map<string, { imageAsset: ImageAsset, autorelease: boolean }>();

    /**
     *
     * @param target 需要设置精灵帧的对象
     * @param bdlename 精灵帧所在的 bundle
     * @param path 精灵帧在 bundle 中的路径
     * @param autorelease 场景切换时是否自动释放
     * @param onComplete 加载完成回调
     * @returns
     */
    public manualLoadSpriteFrame(target: Sprite, bdlename: string, path: string, autorelease = true, onComplete?: (size: Size) => void) {
        this.manualLoadAny(bdlename, path, SpriteFrame, autorelease, (err, asset) => {
            if (err) return Log.e('manualLoadSpriteFrame err: ' + err.message);

            if (target && isValid(target)) {
                target.node.active = true;
                target.spriteFrame = asset;
            }
            onComplete && onComplete(asset!.originalSize);
        });
    }

    /**
     *
     * @param bundlename 需要加载的资源所在 bundle
     * @param path 资源在 bundle 中的路径
     * @param autorelease 场景切换时是否自动释放
     * @param onComplete 加载完成回调
     * @returns
     */
    public manualLoadAny<T extends Asset>(bundlename: string, path: string, type: __private._types_globals__Constructor<T>, autorelease = true, onComplete?: (err: Error | null, asset: T | null) => void) {
        this.loadBundle(bundlename)
            .then(bundle => {
                bundle.load<T>(path, type, (err, asset) => {
                    if (err) return onComplete && onComplete(err, null);
                    if (!this._manualLoaded[path]) {
                        this._manualLoaded[path] = { asset, autorelease };
                        asset.addRef();
                    }
                    onComplete && onComplete(null, asset);
                });
            }).catch(err => { onComplete && onComplete(err, null); });


    }

    /**
     * 释放手动加载的资源，此时会执行资源的 decRef() 以减少引用计数。
     * 但是真正释放与否，取决于是否被依赖。所以这里必须 delete k.
     */
    public releaseManualLoaded() {
        for (let k in this._manualLoaded) {
            let loaded = this._manualLoaded[k],
                assetValid = isValid(loaded.asset);
            if (loaded && loaded.autorelease && assetValid) {
                loaded.asset.decRef();
            }
            if (loaded && assetValid && loaded.asset.refCount <= 0) {
                delete this._manualLoaded[k];
            }
        }

        // 远程ImageAsset
        this._remoteImageAssets.forEach((v, k) => {
            if (v.autorelease) {
                v.imageAsset.decRef();
                this._remoteImageAssets.delete(k);
            }
        });

        // 远程 spine
        this._remoteSpineAssets.forEach((v, k) => {
            if (v.autorelease) {
                v.atlas.decRef();
                v.json.decRef();
                v.png?.decRef();
                this._remoteSpineAssets.delete(k);
            }
        });
    }

    /**
     * 加载 bundles
     * @param onProgress
     */
    public async loadBundles(onProgress?: (progress: number) => void) {
        const names = (this.app.bundleInfo.json as IBundleInfo).bundles;
        let loadbundlenames = names.slice(0);
        loadbundlenames.sort((a, b) => {
            let as = a == WanderCommon.PublicBundle ? 2 : 1;
            let bs = b == WanderCommon.PublicBundle ? 2 : 1;
            return bs - as;
        });
        let total = loadbundlenames.length;
        for (let i = 0; i < total; ++i) {
            try {
                await this.loadBundle(loadbundlenames[i]);
                onProgress && onProgress((i + 1) / total);
            } catch (e) { throw e; }
        }
    }

    /**
     * 加载配置
     * @param onProgress
     * @returns
     */
    public loadConfs(onProgress?: (progress: number) => void): Promise<JsonAsset[]> {
        return new Promise((res, rej) => {
            this.loadBundle(WanderCommon.PublicBundle)
                .then(bundle => {
                    bundle.loadDir<JsonAsset>('cfg', JsonAsset, (loaded: number, total: number) => {
                        onProgress && onProgress(loaded / total);
                    }, (err: Error | null, assets: JsonAsset[]) => {
                        if (err) {
                            rej(err)
                            return;
                        }
                        res(assets);
                    });
                }).catch(rej);
        });
    }

    // 弹框遮罩
    public setPopupMask(visible: boolean, depth: number) {
        if (visible) {
            if (!isValid(this._popupMask)) {
                this._popupMask = instantiate(this.app.popupMaskPfb);
                let root = this.getRoot(WanderCommon.LayerMap.Popup);
                root.addChild(this._popupMask);

                let index = root.children.length - 2 - depth;
                if (index < 0) {
                    index = 0;
                }
                this._popupMask.setSiblingIndex(index);
            }
            this._popupMask!.active = true;
        }
        else if (isValid(this._popupMask)) {
            this._popupMask!.active = false;
        }
    }

    public getToastInst(parent: Node): Node {
        return Pool.getNode(this.app.toastPfb, parent);
    }

    public getLoadingMaskInst(parent: Node): Node {
        return Pool.getNode(this.app.loadingMaskPfb, parent);
    }

    public getWebsocketConnectingInst(parent: Node): Node {
        return Pool.getNode(this.app.websocketConnectingPfb, parent);
    }

    public getRoot(rootName: string): Node {
        let root = this._layerCache[rootName];
        if (isValid(root)) {
            return root;
        }
        this.checkDefaultLayers();
        return this._layerCache[rootName]!;
    }

    ////
    private checkDefaultLayers() {
        let canvas = find('Canvas')!;
        for (let name of WanderCommon.Layers) {
            if (!canvas.getChildByName(name)) {
                let node = new Node(name);
                canvas.addChild(node);
                let comp = node.addComponent(Widget);
                comp.isAlignLeft = comp.isAlignRight = comp.isAlignTop = comp.isAlignBottom = true;
                comp.left = comp.right = comp.top = comp.bottom = 0;
                comp.alignMode = 2;
                comp.enabled = true;
                this._layerCache[name] = node;
            }
        }
    }

    /**
     * 加载 bundle
     * @param name bundle 名
     * @returns
     */
    private loadBundle(name: string): Promise<AssetManager.Bundle> {
        let bundle = assetManager.getBundle(name);
        if (bundle) return Promise.resolve(bundle);
        return new Promise((res, rej) => {
            assetManager.loadBundle(name, (err, _bundle) => {
                if (err) {
                    rej(err);
                    return;
                }
                res(_bundle);
            });
        });
    }

    public get app(): App {
        if (this._app) return this._app;
        return this._app = find('_app_')!.getComponent(App)!;
    }

    //private
    private _app: App = null!;
    private _popupMask: Node | null = null;
    private _layerCache: { [key: string]: Node } = {};
    private _manualLoaded: { [name: string]: { asset: Asset, autorelease: boolean } } = {};

    private _sceneReady = false;
    private _currPreloadedScene: BaseScene = null!;
}

const Res = ResMgr.getInstance() as ResMgr;
export default Res;